/*
 * This handles the following attribute properties
 *  * init with function value in non-lazy initialization
 *  * required attributes in initializaion
 *  * handles for auto-decoration
 *  * predicate for attribute availability checks
 * 
 * 
 * See http://code.google.com/p/joose-js/wiki/JooseAttribute
 */
Joose.Kernel.MetaClass.create('Joose.Attribute', {    
    isa: Joose.Kernel.ProtoAttribute,
    
    before: {
        handleProps: function(classObject){
            this.handleIs(classObject);
        }
    },
    
    after: {
        handleProps: function(classObject){
            this.handlePredicate(classObject);
            this.handleHandles(classObject);
        }
    },
    
    
    methods: {
        
        isPrivate: function () {
            return this.getName().charAt(0) == "_"
        },
        
        
        toPublicName: function () {
            
            if(this.__publicNameCache) { // Cache the publicName (very busy function)
                return this.__publicNameCache
            }
            
            var name = this.getName();
            if(this.isPrivate()) {
                this.__publicNameCache = name.substr(1)
                return this.__publicNameCache;
            }
            this.__publicNameCache = name
            return this.__publicNameCache
        },
        
        
        getIsa: function () {
            var props = this.getProps();
            if("isa" in props && props.isa == null) {
                throw new Error("You declared an isa property but the property is null.")
            }
            if(props.isa) {
                if(!props.isa.meta) {
                    return props.isa()
                }
                return props.isa
            }
            return
        },
        
        
        addSetter: function (classObject) {
            var meta  = classObject.meta;
            var name  = this.getName();
            var props = this.getProps();
            
            var setterName = this.setterName();
            
            if(meta.can(setterName)) { // do not override methods
                return
            }
            
            var isa   = this.getIsa();
    
            var func = this.makeTypeChecker(isa, props, "attribute", name);
            
            meta.addMethod(setterName, func);
        },
        
        
        addGetter: function (classObject) {
            var meta  = classObject.meta;
            var name  = this.getName();
            var props = this.getProps()
            
            var getterName = this.getterName();
            
            if(meta.can(getterName)) { // never override a method
                return 
            }
            
            var func  = function getter () {
                return this[name]
            }
            
            var init  = props.init;
            
            if(props.lazy) {
                func = function lazyGetter () {
                    var val = this[name];
                    if(typeof val == "function" && val === init) {
                        this[name] = val.apply(this)
                    }
                    return this[name]
                }
            }
            
            meta.addMethod(getterName, func);
        },
        
        
        initializerName: function () {
            return this.toPublicName()
        },
        
        
        getterName: function () {
            if(this.__getterNameCache) { // Cache the getterName (very busy function)
                return this.__getterNameCache
            }
            this.__getterNameCache = "get"+Joose.S.uppercaseFirst(this.toPublicName())
            return this.__getterNameCache;
        },
        
        
        setterName: function () {
            if(this.__setterNameCache) { // Cache the setterName (very busy function)
                return this.__setterNameCache
            }
            this.__setterNameCache = "set"+Joose.S.uppercaseFirst(this.toPublicName())
            return this.__setterNameCache;
        },
        
        
        handleIs: function (classObject) {
    //        var name  = this.getName();
            var props = this.getProps();
            
            var is    = props.is;
    
            if(is == "rw" || is == "ro") {
                this.addGetter(classObject);
            }
            if(is == "rw") {
                this.addSetter(classObject)
            }
        },
        
        
        doInitialization: function (object, paras) {
            var  name  = this.initializerName();
            var _name  = this.getName();
            var value;
            var isSet  = false;
            if(typeof paras != "undefined" && typeof paras[name] != "undefined") {
                value  = paras[name];
                isSet  = true;
            } else {
                var props = this.getProps();
                
                var init  = props.init;
                
                if(typeof init == "function" && !props.lazy) {
                    // if init is not a function, we have put it in the prototype, so it is already here
                    value = init.call(object)
                    isSet = true
                } else {
                    // only enforce required property if init is not run
                    if(props.required) {
                        throw "Required initialization parameter missing: "+name + "(While initializing "+object+")"
                    }
                }
            }
            if(isSet) {
                var setterName = this.setterName();
                if(object.meta.can(setterName)) { // use setter if available
                    object[setterName](value)
                } else { // direct attribute access
                    object[_name] = value
                }
            }
        },
        
        
        handlePredicate: function (classObject) {
            var meta  = classObject.meta;
            var name  = this.getName();
            var props = this.getProps();
            
            var predicate = props.predicate;
            
            var getter    = this.getterName();
            
            if(predicate) {
                meta.addMethod(predicate, function () {
                    var val = this[getter]();
                    return val ? true: false
                })
            }
        },
        
        
//XXX TypeChecker as Role (from Joose.TypeChecker)        
//        makeTypeChecker: function (isa, props, thing, name) {
//            var name  = this.getName();
//            
//            return function setter (value) {
//                this[name] = value
//                return this;
//            }
//        },
        
        makeTypeChecker: function (isa, props, thing, name) {
            var func;
            
            if(isa) {
                if(!isa.meta) {
                    throw new Error("Isa declarations in attribute declarations must be Joose classes, roles or type constraints")
                }
                
                var isRole  = false;
                var isType  = false;
                // We need to check whether Joose.Role and Joose.TypeContraint 
                // are there yet, because they might not have been compiled yet
                if(Joose.Role && isa.meta.meta.isa(Joose.Role)) {
                    isRole  = true;
                } 
                else if(Joose.TypeConstraint && isa.meta.isa(Joose.TypeConstraint)) {
                    isType  = true;
                }
                
                // This setter is used if the attribute is constrained with an isa property in the attribute initializer
                // If the isa refers to a class, then the new value must be an instance of that class.
                // If the isa refers to a role,  then the new value must implement that role.
                // If the isa refers to a type constraint, then the value must match that type contraint
                // ...and if the coerce property is set, we try to coerce the new value into the type
                // Throws an exception if the new value does not match the isa property.
                // If errorHandler is given, it will be executed in case of an error with parameters (Exception, isa-Contraint)
                func = function setterWithIsaCheck (val, errorHandler) {
                    var value = val
                    try {
                        if ( props.nullable === true && value == undefined) {
                            // Don't do anything here:)
                        } else if ( isType ) {
                            var newvalue = null;
                            if( props.coerce ) {
                                newvalue = isa.coerce(value);
                            }
                            if ( newvalue == null && props.nullable !== true) {
                                isa.validate(value);
                            } else {
                                value = newvalue;
                            }
                        } else {
                            if(!value || !value.meta) {
                                throw new ReferenceError("The attribute "+name+" only accepts values that have a meta object.")
                            }
                            var typeCheck = isRole ? value.meta.does(isa): value.meta.isa(isa);
                            if( ! typeCheck ) {
                                throw new ReferenceError("The attribute "+name+" only accepts values that are objects of type "+isa.meta.className()+".")
                            }
                        }
                    } catch (e) {
                        if(errorHandler) {
                            errorHandler.call(this, e, isa)
                        } else {
                            throw e
                        }
                    };
                    this[name] = value
                    return this;
                }
            } else {
                func = function setter (value) {
                    this[name] = value
                    return this;
                }
            }
            
            return func;
        },
        

        handleHandles: function (classObject) {
            var meta  = classObject.meta;
            var name  = this.getName();
            var props = this.getProps();
            
            var handles = props.handles;
            var isa     = props.isa
            
            if(handles) {
                if(handles == "*") {
                    if(!isa) {
                        throw "I need an isa property in order to handle a class"
                    }
                    
                    // receives the name and should return a closure
                    var optionalHandlerMaker = props.handleWith;
                    
                    //XXX decorate appears before Joose.Decorator
                    meta.decorate(isa, name, optionalHandlerMaker)
                } 
                else {
                    throw "Unsupported value for handles: "+handles
                }
                
            }
        }
        
    }
    
});